第二十七章:流量控制:使用 if 进行分支

在上一章中,我们遇到了一个问题。我们如何使报告生成器脚本适应运行该脚本的用户的权限?解决这个问题需要我们根据测试结果找到一种在脚本中“改变方向”的方法。在编程方面,我们需要程序分支。

让我们考虑一个用伪代码表示的逻辑的简单例子,这是一种用于人类消费的计算机语言的模拟。

X = 5 If X = 5, then: Say “X equals 5.” Otherwise: Say “X doesis not equal 5.”

这是一个分支的例子。根据条件,“如果X=5”做一件事,“说X等于5”,否则做另一件事“说X不等于5”。

第二十七章:流量控制:使用 if 进行分支if 退出状态test文件表达式字符串表达式整数表达式更现代的 test 版本(( )) - 专为整数设计组合表达式可移植性是小头脑的小妖精控制操作员:另一种分支方式总结

if

使用shell,我们可以对前面的逻辑进行如下编码:

或者我们可以直接在命令行中输入它(稍微缩短)。

在这个例子中,我们执行命令两次;第一次,x的值设置为5,这会导致输出字符串“equals 5”,第二次,x值设置为0,这会输出字符串“does not equal 5”。

if 复合命令具有以下语法:

if commands; then commands [elif commands; then commands...] [else commands] fi

其中 commands 是命令列表。乍一看,这有点令人困惑。但在我们澄清这一点之前,我们必须看看shell如何评估命令的成功或失败。

退出状态

命令(包括我们编写的脚本和shell函数)在终止时向系统发出一个值,称为退出状态(exit status)。此值是0到255范围内的整数,表示命令执行的成功或失败。按照惯例,零值表示成功,任何其他值表示失败。shell提供了一个参数,我们可以使用它来检查命令的退出状态。在这里,我们看到它在行动:

在这个例子中,我们执行 ls 命令两次。第一次,命令成功执行。如果我们显示参数 $? 的值,我们看到它是零。我们再次执行 ls 命令(指定一个不存在的目录),产生错误,并再一次检查参数 $? 。这次它包含一个2,表示命令遇到了错误。一些命令使用不同的退出状态值来提供错误诊断,而许多命令在失败时只是以值1退出。手册页通常包含一个名为“Exit Status”的部分,描述使用的代码。然而,零总是意味着成功。

shell提供了两个极其简单的内置命令,除了以0或1退出状态终止外,什么也不做。 true 命令总是成功执行, false 命令总是失败执行。

我们可以使用这些命令来查看 if 语句是如何工作的。if语句真正做的是评估命令的成功或失败。

命令 echo “It's true.”if 命令成功执行时执行,在 if 命令未成功执行时不执行。如果命令列表遵循 if,则计算列表中的最后一个命令:

test

到目前为止, if 最常用的命令是 testtest 命令执行各种检查和比较。它有两种等效形式。第一个,如下所示:

test expression

第二种,更受欢迎的形式,如下所示:

[ expression ]

其中 expression 是被评估为真或假的表达式。当表达式为真时, test 命令返回退出状态0,当表达式为假时,返回状态1。

值得注意的是, test[ 实际上都是命令。在bash中,它们是内置的,但它们也作为程序存在于 /usr/bin 中,供其他shell使用。表达式实际上只是它的参数, [ 命令要求 ] 字符作为其最终参数提供。

test[ 命令支持各种有用的表达式和测试。

文件表达式

下表列出了用于评估文件状态的表达式。

表达式为true的情况
file1 -ef file2file1file2 具有相同的索引节点号
这两个文件名通过硬链接引用同一个文件。
file1 -nt file2file1 is newer than file2.
file1 -ot file2file1 is older than file2.
-b filefile存在,且是块专用(block-special,设备)文件。
-c filefile存在,且是一个字符特殊(character-special,设备)文件。
-d filefile存在,且是一个目录。
-e filefile存在。
-f filefile存在,且是一个普通文件。
-g filefile存在,且设置了组ID
-G filefile存在,并由有效组ID拥有。
-k filefile存在,并且设置了“粘性位”。
-L filefile存在,是一个符号链接。
-O filefile存在,并由有效用户ID拥有。
-p filefile存在,并且是一个命名管道。
-r filefile存在并且可读(对有效用户具有可读权限)。
-s filefile存在并且长度大于零。
-S filefile存在,并且是一个网络套接字。
-t fdfd 是指向/来自终端的文件描述符。
这可用于确定是否重定向了标准输入/输出/错误。
-u filefile存在并且是setuid。
-w filefile存在并且可写(对有效用户具有写权限)。
-x filefile存在并且可执行(对有效用户具有执行/搜索权限)。

这里我们有一个脚本,演示了一些文件表达式:

该脚本计算分配给常量 FILE 的文件,并在执行计算时显示其结果。关于这个脚本,有两点值得注意。

首先,请注意参数 $FILE 在表达式中的引用方式。这不是语法上完成表达式所必需的;相反,它是对参数为空或仅包含空格的防御。如果 $FILE 的参数扩展导致空值,则会导致错误(运算符将被解释为非空字符串而不是运算符)。在参数周围使用引号可确保运算符后面始终跟有字符串,即使字符串为空。

其次,请注意脚本末尾附近存在 exit 命令。 exit 命令接受一个可选参数,该参数将成为脚本的退出状态。当没有传递参数时,退出状态默认为最后执行的命令的退出状态。以这种方式使用 exit 允许脚本在 $FILE 扩展为不存在的文件名时指示失败。出现在脚本最后一行的 exit 命令是一种形式。当脚本“runs off the end”(到达文件末尾)时,它会以执行的最后一个命令的退出状态终止。

同样,shell函数可以通过在 return 命令中包含整数参数来返回退出状态。如果我们将之前的脚本转换为shell函数以将其包含在更大的程序中,我们可以用 return 语句替换 exit 命令并获得所需的行为。

字符串表达式

下表列出了用于计算字符串的表达式:

表达式为true的情况
stringstring 不为空
-n stringstring 长度大于零
-z stringstring 长度为零
string1 = string2
string1 == string2
string1string2 相等。
可以使用单等号或双等号。
bash支持使用双等号,通常是首选,但它不符合POSIX标准。
string1 != string2string1string2 不同
string1 > string2string1string2 之后排序。
string1 < string2string1string2 之前排序。

警告:与测试一起使用时, >< 表达式运算符必须加引号(或用反斜杠转义)。如果不是,它们将被shell解释为重定向运算符,可能会产生破坏性的结果。还要注意,虽然bash文档指出排序顺序符合当前区域设置的排序顺序,但可能不符合。ASCII(POSIX)顺序用于bash 4.0之前的版本。此问题已在4.1版本中修复。

这是一个包含字符串表达式的脚本:

在这个脚本中,我们计算常量ANSWER 。我们首先确定字符串是否为空。如果是,我们终止脚本并将退出状态设置为1。请注意应用于 echo 命令的重定向。这将错误消息“There is no answer.”重定向到标准错误,这是处理错误消息的正确方法。如果字符串不为空,我们会计算字符串的值,看看它是否等于“yes”、“no”或“maybe”。我们使用 elif 来实现这一点, elifelse if 的缩写。通过使用 elif ,我们能够构建更复杂的逻辑测试。

整数表达式

为了将值作为整数而不是字符串进行比较,我们可以使用下表中列出的表达式。

表达式为true的情况
integer1 -eq integer2integer1 等于 integer2
integer1 -ne integer2integer1 不等于 integer2
integer1 -le integer2integer1 小于或等于 integer2 (less than or equal)
integer1 -lt integer2integer1 小于 integer2 (less than)
integer1 -ge integer2integer1 大于或等于 integer2 (greater than or equal)
integer1 -gt integer2integer1 大于 integer2 (greater than)

以下是一个演示它们的脚本:

该脚本的有趣之处在于它如何确定整数是偶数还是奇数。通过对数字执行模2运算,将数字除以2并返回余数,它可以判断数字是奇数还是偶数。

更现代的 test 版本

现代版本的bash包含一个复合命令,可以作为 test 的增强替代品。它使用以下语法:

[[ expression ]]

其中,与 test 一样, expression 是一个计算结果为真或假的表达式。 [[ ]] 命令类似于 test (它支持其所有表达式),但添加了一个重要的新字符串表达式。

string1 =~ regex

如果 string1 与扩展正则表达式 regex 匹配,则返回 true 。这为执行数据验证等任务开辟了很多可能性。在前面的整数表达式示例中,如果常量INT包含除整数之外的任何内容,则脚本将失败。脚本需要一种方法来验证常量是否包含整数。使用 [[ ]]=~ 字符表达式运算符,我们可以这样改进脚本:

通过应用正则表达式,我们能够将 INT 的值限制为仅以可选减号开头,后跟一个或多个数字的字符串。此表达式还消除了空值的可能性。

[[ ]] 的另一个附加功能是 == 运算符支持模式匹配,就像路径名扩展一样。这里有一个例子:

这使得 [[ ]] 在评估文件名和路径名时非常有用。

(( )) - 专为整数设计

除了 [[ ]] 复合命令外,bash还提供了 (( )) 复合命令,这对整数操作很有用。它支持一整套算术运算,我们将在 【第34章.字符串和数字】中全面介绍这一主题。

(( )) 用于执行算术真值测试(arithmetic truth tests)。如果算术求值的结果为非零,则算术真值测试结果为真。

使用 (( )) ,我们可以稍微简化 test-integer2 脚本,如下所示:

请注意,我们使用小于和大于符号, == 用于测试等价性。这是一种处理整数时看起来更自然的语法。还要注意,由于复合命令 (( )) 是shell语法的一部分,而不是普通命令,而且它只处理整数,因此它能够按名称识别变量,不需要执行扩展。我们将在【第34章】中进一步讨论 (( )) 和相关的算术展开。

组合表达式

还可以组合表达式来创建更复杂的计算。表达式通过使用逻辑运算符组合在一起。当我们学习 find 命令时,我们在【第17章.搜索文件】中看到了这些。 test[[ ]] 有三个逻辑操作。它们是 ANDORNOTtest[[ ]] 使用不同的运算符来表示这些操作:

操作test[[ ]] 和 (( ))
AND-a&&
OR-o||
NOT!!

这是一个 AND 操作的示例。以下脚本确定整数是否在值范围内:

在这个脚本中,我们确定整数 INT 的值是否位于 MIN_VALMAX_VAL 的值之间。这是通过单次使用 [[ ]] 来执行的,其中包括由 && 运算符分隔的两个表达式。我们也可以使用 test 对其进行编码:

! 否定运算符反转表达式的结果。如果表达式为 false ,则返回 true ;如果表达式为 true ,则返回 false 。在下面的脚本中,我们修改了计算的逻辑,以找到指定范围之外的 INT 值:

我们还在表达式周围加上括号,用于分组。如果不包括这些,否定只适用于第一个表达式,而不适用于两者的组合。使用测试对其进行编码的方式如下:

由于 test 使用的所有表达式和运算符都被shell视为命令参数(与 [[ ]](( )) 不同),因此对bash具有特殊含义的字符,如 <>() ,必须被引用或转义。

看到 test[[ ]] 做大致相同的事情,哪一个更可取? test 是传统的(也是标准shell的POSIX规范的一部分,通常用于运行系统启动脚本),而 [[ ]] 特定于bash(和其他一些现代shell)。了解如何使用 test 很重要,因为它被广泛使用,但 [[ ]] 显然更有用,更容易编码,因此它是现代脚本的首选。

可移植性是小头脑的小妖精

Hobgoblin —— 妖怪,大地精;淘气鬼,怪物;大哥布林

portability —— 可移植性,可携性

如果你和“真正的”Unix用户交谈,你很快就会发现他们中的许多人不太喜欢Linux。他们认为这是不洁的。Unix用户的一个原则是,一切都应该是“可移植的”。这意味着你写的任何脚本都应该能够在任何类Unix系统上运行,不受更改。

Unix用户有充分的理由相信这一点。在POSIX之前,他们已经看到了命令和shell的专有扩展对Unix世界的影响,他们自然对Linux对他们心爱的操作系统的影响持谨慎态度。

但可移植性有一个严重的缺点。它阻碍了进步。它要求事情总是使用“最低公分母”技术来完成。在shell编程的情况下,这意味着使所有内容都与 sh 兼容, sh 是原始的Bourne shell。

这种缺点是专有软件供应商用来为其专有扩展辩护的借口,只是他们称之为“创新”。但它们实际上只是客户的锁定设备。

GNU工具,如bash,没有这样的限制。它们通过支持标准和普遍可用来鼓励可移植性。你可以在几乎任何类型的系统上安装bash和其他GNU工具,甚至是Windows,而无需付费。所以,可以随意使用bash的所有功能。它真的很便携。

控制操作员:另一种分支方式

bash提供了两个可以执行分支的控制运算符。 && (AND)和 || (OR)运算符的工作方式与 [[ ]] 复合命令中的逻辑运算符相似。以下是 && 的语法:

command1 && command2

以下是 || 的语法:

command1 || command2

了解这些行为很重要。

使用&&运算符时,始终执行command1,只有当command1成功时才执行command2

使用||运算符时,始终执行command1,只有当command1成功时才执行command2

实际上,这意味着我们可以做这样的事情:

这将创建一个名为 temp 的目录,如果成功,当前工作目录将更改为 temp 。只有当 mkdir 命令成功时,才会尝试执行第二个命令。同样,这样的命令:

将测试目录 temp 的存在,只有测试失败,才会创建目录。这种构造对于处理脚本中的错误非常方便,我们将在后面的章节中对此进行更多讨论。例如,我们可以在脚本中这样做:

如果脚本需要临时目录,但该目录不存在,则脚本将终止,退出状态为1。

记住,如果我们有做复杂事情的冲动,命令可以是组命令:

组命令返回组中最后一个命令的退出状态。

总结

我们以一个问题开始了这一章。我们如何使 sys_info_page 脚本检测用户是否有权读取所有主目录?根据我们对 if 的了解,我们可以通过将以下代码添加到 report_home_space 函数中来解决这个问题:

我们评估 id 命令的输出。使用 -u 选项, id 输出有效用户的数字用户 id 号。超级用户的ID始终为零,其他所有用户都是大于零的数字。知道这一点后,我们可以在这里构建两个不同的文档,一个利用超级用户权限,另一个仅限于用户自己的主目录。

我们将暂停 sys_info_page 程序,但别担心。它会回来的。与此同时,我们将讨论一些我们在恢复工作时需要的主题。